(ns Machine)
;*****************************************************************************************

(defn add-bill
  "add bill to a list of bills"
  [bills bill]
  (dosync
    (alter bills conj (ref bill))))

(defn print-all-bills
  "print all bills"
  [bills]
  (doseq [b @bills]
    (println (str "Type of bill:" (@b :type) ". Number of that type:" (@b :number) "."))
    ) )

(defn decrease-number
  "decrease number of bill for the input number"
  [bill  number-new]
  (dosync (ref-set bill {:type (bill :type) :number (- (bill :number) number-new)})))

(defn decrease-number-of-bill 
  "find the type of bill in a map of bills and decrease number of founded bill for the input number"
  [number type-of-bill map-of-bills]
  (if (= type-of-bill ((first map-of-bills) :type))
    (do
      (decrease-number (first map-of-bills)  number)
      true
      )  
    (do
      false
      (decrease-number-of-bill number type-of-bill (rest map-of-bills)))
    ) )

(defn select-values 
   "this fn is founded on the internet; this function returns a new map which include only a pair
 key-value for the input keys"
  [m ks] 
         (map m ks))   

(defn apply-values 
   "this fn is founded on the internet;"
  [map f & ks]      
         (apply f (select-values map ks)))

(defn sum-all 
  "return a sum of all elements which include in the map"
  [my-map s]
  ""
  (if(= nil (first my-map))
      s
      (sum-all (rest my-map) (+ s (apply-values (first my-map) * :type :number) ) ) )  )

(defn check-input-ammount
  "check if the number is partible with x"
  [ammount x]
  (if(= 0 (rem ammount x))
    true
    false
  ) )


(defn check-automat 
  "check if the input ammount is less then sum of all elements in the input map"
  [sum my-map]
  (if (> sum (sum-all my-map 0))
    false
    true
    ) )


(def user-ammounts
  (ref []))

(defn add-user-ammount
  "add user ammount to a list of ammounts"
  [ammounts ammount]
  (dosync
    (alter ammounts conj ammount)) )

(defn print-user-ammounts
  "print all ammounts for that day"
  [ammounts]
  (doseq [a @ammounts]
    (println (str "Ammount: " a) ) ))

(defn find-the-min-type-of-bill-for-the-input-number
  "finds a minimal type of bill which is available"
  [ammount map-of-bills map-of-bills-pom]
  (if (and (<= ammount ((first map-of-bills) :type) ) (>= ammount ((first (rest map-of-bills)) :type)) )  
     (if (>= ((first (rest map-of-bills)) :number) 1)
         ( (first (rest map-of-bills) ) :type)
         (if (>= ((first  map-of-bills) :number) 1)
              ((first map-of-bills) :type)
              (find-the-min-type-of-bill-for-the-input-number (+ ammount ( (first (rest map-of-bills) ) :type) ) map-of-bills-pom map-of-bills-pom)
          )
         )
   (find-the-min-type-of-bill-for-the-input-number ammount (rest map-of-bills) map-of-bills-pom)
    ) )

(defn give-me-money
  [ammount return-map my-map-of-bills user-ammounts a2 map-pom]
  "return a map of combination typeOfBills-numberOfBills for input sum 
   depending on avalible numbers of different types of bills"
  (if (= true (check-automat ammount map-pom))
    (if (= nil (first my-map-of-bills)) 
    (do
      (println "Your map of type and number of bills are....")
      (add-user-ammount user-ammounts a2)
      return-map
      )
    (if (> ammount (sum-all my-map-of-bills 0)) 
        (do
          (add-user-ammount user-ammounts a2)
          (println (str "There is enough money but there is no combination type-number
                    for ammount " a2 
                     " but if you type other number you could get minimal nearest ammount."))
          (println "Minimal of two nearest number to your ammount that is available is:")
          (find-the-min-type-of-bill-for-the-input-number a2 map-pom map-pom)
          )
        (do
         (if ( <  (first (select-values (first my-map-of-bills) [:number])) (quot ammount (first(select-values (first my-map-of-bills) [:type])) ))
           (do
             (decrease-number-of-bill (first (select-values (first my-map-of-bills) [:number])) (first (select-values (first my-map-of-bills) [:type])) my-map-of-bills)
             (give-me-money
                (- ammount (* (first (select-values (first my-map-of-bills) [:number])) (first (select-values (first my-map-of-bills) [:type]))) )
                (assoc return-map (first (select-values (first my-map-of-bills) [:type])) (first (select-values (first my-map-of-bills) [:number])))
                (rest my-map-of-bills)
                user-ammounts 
                a2
                map-pom
                )
          ) 
           (do
              (decrease-number-of-bill (quot ammount (first (select-values (first my-map-of-bills) [:type]))) (first (select-values (first my-map-of-bills) [:type])) my-map-of-bills)
             (give-me-money
                (rem ammount (first (select-values (first my-map-of-bills) [:type])))
                (assoc return-map (first (select-values (first my-map-of-bills) [:type])) (quot ammount (first (select-values (first my-map-of-bills) [:type]))))
                (rest my-map-of-bills) 
                user-ammounts
                a2
                map-pom
                )
          ))   
             
        ) )
      )
  
    (do
      (add-user-ammount user-ammounts a2)
      (println (str "There *** is not enough money in our machine...Sorry...You try to get: " a2 ))
      )
  )
  
  )

(defn sort-user-ammounts 
  "sort a vector"
  [ammounts]
  (dosync
    (ref-set ammounts (sort @ammounts) )))

(defn get-mediana 
  "return a mediana for the sorted array"
  [sorted-array]
  (if (= (rem (count sorted-array) 2) 0)
    ( / (+ (get sorted-array (- (/ (count sorted-array) 2) 1)) (get sorted-array (/ (count sorted-array) 2) ) ) 2)
    (get sorted-array (quot (count sorted-array) 2 )) 
    ) )

(defn make-report
  [sum  array-of-type-of-bill final-report]
  (if(= 0 (count array-of-type-of-bill)) final-report
    (if (< (quot (quot sum 2) (first array-of-type-of-bill) ) 0)
      (do
        
        (make-report sum (rest array-of-type-of-bill)  (conj final-report {:type (first array-of-type-of-bill) :number 10}))
        )
      (do
      (make-report 
        (- sum (* (first array-of-type-of-bill) (quot (quot sum 2) (first array-of-type-of-bill) ) ) ) 
        (rest array-of-type-of-bill) 
        (conj final-report {:type (first array-of-type-of-bill) :number (+ (quot (quot sum 2) (first array-of-type-of-bill) ) 10)}))
      ) )
    )
  )



;*********************************************************************************************

;reset of every map and initial update for the cash machine
(defn reset-user-map 
  "reset list of desired ammounts that users would get for that day"
   []
   (def user-ammounts(ref [])))

(defn add-all-bills 
  "add map of pairs typeOfBill-numbersOfThetType to a cash machine"
  []
 (do
 (reset-user-map )
 (def my-bills(ref []))
 (add-bill my-bills {:type 1000 :number 2 })
 (add-bill my-bills {:type 500 :number 1 })
 (add-bill my-bills {:type 200 :number 2 })
 (add-bill my-bills {:type 100 :number 1})
 (add-bill my-bills {:type 50 :number  0 }) ) )

 (defn add-number-for-bill
   "add new map of pairs type-number to our machine which is result of fn named make-report"
   [map-report]
   (if (= 0 (count map-report))
     (println "Map of bills are added to a cash machine so it's ready for use...")
     (do
       (println (first map-report))
       (add-bill my-bills (first map-report))
       (add-number-for-bill (rest map-report)))  ) )

;test for users*******************************************************************************
(defn test-give-me-money 
  "check if you input number is divisible with x and then if it's true give money to a user"
   [sum x]
(do
  (if (= true (check-input-ammount sum x))
  (give-me-money sum {} @my-bills user-ammounts sum @my-bills)
  (println (str "Input number must be divisible by " x))
  )
)
)
 
(defn test-give-me-a-new-map-after-update
  "print a new map after giving money to a customer"
  [a]
  (do
  (println (str "Map of type-bills after giving ammount of :" a ) )
  (print-all-bills my-bills) ))

 (defn test-make-report
   [vec-of-types]
   (do
   (println "List of user's input ammounts for that day is: ")
   (print-user-ammounts user-ammounts)
   (println "This is the REPORT for the next day which tells us 
    how many bills of which type should be in the machine
    so mashine has enough money and
    every user could get desired ammount")
   (make-report (* (count @user-ammounts) (get-mediana @user-ammounts) ) vec-of-types []))
   )

 ;this is the end
 ;you can now explore this code:)
 (println "******************************explore***********************************************")
 ;adding bills and printing a start map in the cash mashine
 (add-all-bills )

 
 ;user try to get money from the cash machine
 ;after getting money you can print type and number of bills which are left in the machine
 ;after transaction
 (test-give-me-money 2700 50)
 (test-give-me-a-new-map-after-update 2450)
 
 ;the result is error message for the user about non-divisible with 50 
 (test-give-me-money  3557 50)

 ;the result is error message about not having enough money in the machine
 (test-give-me-money 15000 50)
 
 ;the result is that there is enough money bat you have to change your input number
 ;to minimal of two nearest number for your ammount
 ;because there isn't enough number of bill for you combination
 (test-give-me-money  150 50)
 
 (test-give-me-money 300 50)
 (test-give-me-a-new-map-after-update 300)
  
 
 ;print a report for the next day which tells us 
 ;how many bills of which type should be in the machine
 ;so mashine has enough money and
 ;every user could get desired ammount
 
 (test-make-report [1000 500 200 100 50])
 
 ;list of user ammounts include or input number without number that isn't divisible with 50
 ;(println "List of user's input ammounts for that day is: ")
 ;(print-user-ammounts user-ammounts)
 
 ;you can test a given report if you want to
 ;(println "Let's try our report: ")
 ;(def my-bills(ref []))
 ;(add-number-for-bill (test-make-report [1000 500 200 100 50]))
 ;(print-all-bills my-bills)
 ;(reset-user-map )
 
 

